//
// Created by mini on 2021/7/29.
//

import Foundation
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif


public extension View {
    var relativeParams: RelativeParams {
        get {
            ensureAttr(key: "relativeParams") {
                RelativeParams()
            }
        }
    }

    @discardableResult
    func relativeParams(_ block: (RelBuilder) -> Void) -> Self {
        let b = RelBuilder()
        block(b)
        self.relativeParams.conditions.append(contentsOf: b.items)
        for item in b.items {
            if item.prop == .width || item.prop == .height {
                if item.constant == WrapContent {
                    self.needLayoutOnPropChanged()
                }
            }
        }
        return self
    }
}

public enum RelProp: Int {
    case width, height
    case left, right, top, bottom
    case centerX, centerY

}

fileprivate let UNSPEC: CGFloat = -10000

public class RelCondition: Applyable {
    fileprivate weak var view: View?
    fileprivate weak var toView: View? = nil
    public var prop: RelProp
    public var otherView: OtherView? = nil
    public var otherProp: RelProp? = nil //if nil, use prop value
    public var multiplier: CGFloat = 1
    public var constant: CGFloat = 0

    fileprivate var tempValue: CGFloat = UNSPEC
    fileprivate var OK: Bool {
        tempValue != UNSPEC
    }

    public init(prop: RelProp) {
        self.prop = prop
    }
}

public class RelativeParams {
    public var conditions: [RelCondition] = []

    @discardableResult
    public func updateConstant(_ prop: RelProp, _ constant: CGFloat) -> Self {
        guard let c = self.conditions.first({ c in c.prop == prop }) else {
            return self
        }
        c.constant = constant
        c.view?.superview?.postLayout()
        return self
    }

    @discardableResult
    public func updateConstantIf(_ constant: CGFloat, _ block: (RelCondition) -> Bool) -> Self {
        for c in conditions {
            if block(c) {
                c.constant = constant
                c.view?.superview?.postLayout()
                break
            }
        }
        return self
    }
}


public class RelBuilder {
    var items = [RelCondition]()
}

public extension RelBuilder {

    @discardableResult
    func below(_ other: OtherView) -> RelBuilderEnd {
        top.eq(other, .bottom)
    }

    @discardableResult
    func above(_ other: OtherView) -> RelBuilderEnd {
        bottom.eq(other, .top)
    }

    @discardableResult
    func toRightOf(_ other: OtherView) -> RelBuilderEnd {
        left.eq(other, .right)
    }

    @discardableResult
    func toLeftOf(_ other: OtherView) -> RelBuilderEnd {
        right.eq(other, .left)
    }

    private func make(_ ls: [RelProp]) -> RelBuilderEnd {
        let cs = ls.map {
            RelCondition(prop: $0)
        }
        items.append(contentsOf: cs)
        return RelBuilderEnd(cs)
    }

    var size: RelBuilderEnd {
        make([.width, .height])
    }

    var width: RelBuilderEnd {
        make([.width])
    }
    var height: RelBuilderEnd {
        make([.height])
    }
    var centerX: RelBuilderEnd {
        make([.centerX])
    }
    var centerY: RelBuilderEnd {
        make([.centerY])
    }
    var left: RelBuilderEnd {
        make([.left])
    }
    var right: RelBuilderEnd {
        make([.right])
    }
    var top: RelBuilderEnd {
        make([.top])
    }
    var bottom: RelBuilderEnd {
        make([.bottom])
    }
    var center: RelBuilderEnd {
        make([.centerX, .centerY])
    }
    var edgeX: RelBuilderEnd {
        make([.left, .right])
    }
    var edgeY: RelBuilderEnd {
        make([.top, .bottom])
    }
    var edges: RelBuilderEnd {
        make([.left, .top, .right, .bottom])
    }
    var leftTop: RelBuilderEnd {
        make([.left, .top])
    }
}

public class RelBuilderEnd {
    fileprivate var cs: [RelCondition]

    init(_ ls: [RelCondition]) {
        self.cs = ls
    }

    public func multi(_ m: CGFloat) {
        for item in cs {
            item.multiplier = m
        }
    }

    public func constant(_ c: CGFloat) {
        for item in cs {
            item.constant = c
        }
    }

    public func constant(values: CGFloat...) {
        if values.count != cs.count {
            fatalError("长度不一致")
        }
        for i in 0..<cs.count {
            cs[i].constant = values[i]
        }
    }

    public func inset(_ c: CGFloat) {
        for item in cs {
            switch item.prop {
            case .right, .bottom, .width, .height:
                item.constant = -c
            default:
                item.constant = c
            }
        }
    }

    public func inset(values: CGFloat...) {
        if values.count != cs.count {
            fatalError("长度不一致")
        }
        for i in 0..<cs.count {
            let item = cs[i]
            switch item.prop {
            case .right, .bottom, .width, .height:
                item.constant = -values[i]
            default:
                item.constant = values[i]
            }
        }
    }

    public func wrap() {
        for item in cs {
            if item.prop != .width && item.prop != .height {
                fatalError("Wrap only used by .width or .height")
            }
            item.constant = WrapContent
        }


    }
}

public extension RelBuilderEnd {

    @discardableResult
    func eqParent(_ prop: RelProp? = nil) -> Self {
        eq("..", prop)
    }

    @discardableResult
    func eqSelf(_ prop: RelProp) -> Self {
        eq(".", prop)
    }

    func eq(values: CGFloat...) {
        if values.count != cs.count {
            fatalError("长度不一致")
        }
        for i in 0..<cs.count {
            cs[i].constant = values[i]
        }
    }

    @discardableResult
    func eq(_ value: CGFloat) -> Self {
        for item in cs {
            item.constant = value
        }
        return self
    }

    @discardableResult
    func eq(_ other: OtherView, _ prop: RelProp? = nil) -> Self {
        for item in cs {
            item.otherView = other
            item.otherProp = prop ?? item.prop
        }
        return self
    }
}


public class RelativeLayout: BaseLayout {
    public var padding: Edge = Edge() {
        didSet {
            postLayout()
        }
    }

    var boundsPaded: Rect {
        self.bounds.inset(padding)
    }

    public var viewMaxY: UIView? = nil
    public var autoSize: Bool = false

    override func layoutChildren(_ calcSize: CGSize?) -> CGSize {
        let boundsInseted = self.boundsPaded

        let childList = self.subviews
        if childList.isEmpty {
            return .zero
        }

        var allCond: [RelCondition] = []
        for child in childList {
            for cond in child.relativeParams.conditions {
                cond.view = child
                cond.toView = child.find(otherView: cond.otherView)
                cond.tempValue = UNSPEC
                allCond.append(cond)
            }
        }


        let vrList: ViewRects = ViewRects(childList)

        for vr in vrList.items {
            if vr.view.relativeParams.conditions.isEmpty {
                vr[.left] = 0
                vr[.right] = 0
                vr[.width] = 0
                vr[.height] = 0
            }
        }

        for c in allCond {
            if c.toView == nil {
                switch c.constant {
                    #if os(iOS)
                case WrapContent:
                    let sz = wrapSizeOf(view: c.view!, calcSize ?? boundsInseted.size)
                    if c.prop == .width {
                        c.tempValue = max(sz.width, 0)
                    } else if c.prop == .height {
                        c.tempValue = max(sz.height, 0)
                    } else {
                        fatalError("WrapContent ONLY used on width or height property")
                    }
                    #endif
                case MatchParent:
                    if c.prop == .width || c.prop == .height {
                        c.tempValue = queryProp(rect: boundsInseted, c.prop)
                    } else {
                        fatalError("WrapContent ONLY used on width or height property")
                    }
                    break
                default:
                    c.tempValue = c.constant
                }
                vrList.assignProp(c.view!, c.prop, c.tempValue)
            } else if c.toView === self {
                let otherProp = c.otherProp ?? c.prop
                c.tempValue = queryProp(rect: boundsInseted, otherProp) * c.multiplier + c.constant
                vrList.assignProp(c.view!, c.prop, c.tempValue)
            }

        }

        var matchOne = false
        repeat {
            matchOne = false
            let notMatchList = allCond.filter {
                !$0.OK
            }
            if notMatchList.isEmpty {
                break
            }
            for c in notMatchList {
                guard  let otherView = c.toView else {
                    continue
                }
                let otherProp = c.otherProp ?? c.prop
                let otherVal = vrList.queryProp(otherView, otherProp)
                if otherVal != UNSPEC {
                    c.tempValue = otherVal * c.multiplier + c.constant
                    vrList.assignProp(c.view!, c.prop, c.tempValue)
                    matchOne = true
                }
            }
        } while matchOne

        let notMatchList = allCond.filter {
            !$0.OK
        }
        if !notMatchList.isEmpty {
            print("WARNNING! RelativeLayout: some condition is NOT satisfied ! ")
        }


        var maxX: CGFloat = boundsInseted.minX
        var maxY: CGFloat = boundsInseted.minY

        var mV: UIView? = nil
        for vr in vrList.items {
            if vr.OK {
                if calcSize == nil {
                    vr.view.constraintStorage.updateFrame(vr.rect)
                }
                maxX = max(maxX, vr[.right])
//                maxY = max(maxY, vr[.bottom])
                if vr[.bottom] > maxY {
                    maxY = vr[.bottom]
                    mV = vr.view
                }
            }
        }
        if self.viewMaxY !== mV {
            if autoSize {
                if let old = self.viewMaxY {
                    old.constraintStorage.remove(ident: "bottom_eq_parentRel")
                }
                if let newV = mV {
                    newV.constraints { c in
                        c.bottom.eqParent().constant(-self.padding.bottom).ident("bottom_eq_parentRel").priority(.defaultHigh)
                    }
                }
            }
            self.viewMaxY = mV
        }

        var sz = CGSize(width: maxX - boundsInseted.minX, height: maxY - boundsInseted.minY)
        sz.width += padding.horSpace
        sz.height += padding.verSpace
        return sz
    }


    private func queryProp(rect: Rect, _ prop: RelProp) -> CGFloat {
        switch prop {
        case .left:
            return rect.minX
        case .right:
            return rect.maxX
        case .top:
            return rect.minY
        case .bottom:
            return rect.maxY
        case .centerX:
            return rect.center.x
        case .centerY:
            return rect.center.y
        case .width:
            return rect.width
        case .height:
            return rect.height
        }
    }
}


fileprivate class ViewRect {
    var view: View

    private var valuMap: [RelProp: CGFloat] = [:]

    subscript(prop: RelProp) -> CGFloat {
        get {
            valuMap[prop] ?? UNSPEC
        }
        set {
            valuMap[prop] = newValue
            switch prop {
            case .left, .right, .centerX, .width:
                checkHor()
            default:
                checkVer()
            }
        }
    }

    private var checking = false

    init(_ view: View) {
        self.view = view
    }

    var OK: Bool {
        atLeast2([.width, .left, .right, .centerX]) >= 2 && atLeast2([.height, .top, .bottom, .centerY]) >= 2
    }

    var rect: Rect {
        Rect(x: self[.left], y: self[.top], width: self[.width], height: self[.height])
    }


    private func checkVer() {
        if checking {
            return
        }
        checking = true
        let n = atLeast2([.height, .top, .bottom, .centerY])
        if n == 2 || n == 3 {
            doCheckVer()
        }
        checking = false
    }

    private func doCheckVer() {
        if self[.height] != UNSPEC && self[.top] != UNSPEC {
            self[.bottom] = self[.top] + self[.height]
            self[.centerY] = (self[.top] + self[.bottom]) / 2
            return
        }
        if self[.height] != UNSPEC && self[.bottom] != UNSPEC {
            self[.top] = self[.bottom] - self[.height]
            self[.centerY] = (self[.top] + self[.bottom]) / 2
            return
        }
        if self[.height] != UNSPEC && self[.centerY] != UNSPEC {
            self[.top] = self[.centerY] - self[.height] / 2
            self[.bottom] = self[.centerY] + self[.height] / 2
            return
        }

        if self[.top] != UNSPEC && self[.bottom] != UNSPEC {
            self[.height] = self[.bottom] - self[.top]
            self[.centerY] = (self[.top] + self[.bottom]) / 2
            return
        }
        if self[.top] != UNSPEC && self[.centerY] != UNSPEC {
            self[.bottom] = self[.centerY] * 2 - self[.top]
            self[.height] = self[.bottom] - self[.top]
            return
        }

        if self[.bottom] != UNSPEC && self[.centerY] != UNSPEC {
            self[.height] = (self[.bottom] - self[.centerY]) * 2
            self[.top] = self[.bottom] - self[.height]
        }
    }


    private func checkHor() {
        if checking {
            return
        }
        checking = true
        let n = atLeast2([.width, .left, .right, .centerX])
        if n == 2 || n == 3 {
            doCheckHor()
        }
        checking = false
    }

    private func doCheckHor() {

        //任意两个决定另外两个
        if self[.width] != UNSPEC && self[.left] != UNSPEC {
            self[.right] = self[.left] + self[.width]
            self[.centerX] = (self[.left] + self[.right]) / 2
            return
        }
        if self[.width] != UNSPEC && self[.right] != UNSPEC {
            self[.left] = self[.right] - self[.width]
            self[.centerX] = (self[.left] + self[.right]) / 2
            return
        }
        if self[.width] != UNSPEC && self[.centerX] != UNSPEC {
            self[.left] = self[.centerX] - self[.width] / 2
            self[.right] = self[.centerX] + self[.width] / 2
            return
        }

        if self[.left] != UNSPEC && self[.right] != UNSPEC {
            self[.width] = self[.right] - self[.left]
            self[.centerX] = (self[.left] + self[.right]) / 2
            return
        }
        if self[.left] != UNSPEC && self[.centerX] != UNSPEC {
            self[.right] = self[.centerX] * 2 - self[.left]
            self[.width] = self[.right] - self[.left]
            return
        }

        if self[.right] != UNSPEC && self[.centerX] != UNSPEC {
            self[.width] = (self[.right] - self[.centerX]) * 2
            self[.left] = self[.right] - self[.width]
        }
    }

    private func atLeast2(_ ls: [RelProp]) -> Int {
        var n = 0
        for a in ls {
            if self[a] != UNSPEC {
                n += 1
            }
        }
        return n
    }

    func getProp(_ prop: RelProp) -> CGFloat {
        self[prop]
    }

    func setProp(_ prop: RelProp, _ value: CGFloat) {
        self[prop] = value
    }
}

fileprivate class ViewRects {
    var items: [ViewRect]

    init(_ ls: [View]) {
        items = ls.map {
            ViewRect($0)
        }
    }

    private func byView(_ view: View) -> ViewRect {
        guard  let vr = items.first({
            $0.view === view
        }) else {
            fatalError("Relative Layout Error: relative view \(view) NOT in subviews")
        }
        return vr
    }

    func queryProp(_ view: View, _ prop: RelProp) -> CGFloat {
        byView(view)[prop]
    }

    func assignProp(_ view: View, _ prop: RelProp, _ value: CGFloat) {
        byView(view).setProp(prop, value)
    }
}